using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;

namespace TelnetExpect
{
    /// <summary>
    /// Provides Telnet communications and in negotiation rejects all options.
    /// </summary>
    public class TelnetBaseStream : Stream
    {
        /// <summary>
        /// The default TCP/IP port used by Telnet.
        /// </summary>
        public const int DefaultPort = 23;
        //
        protected const byte SE = 240;
        protected const byte SB = 250;
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased", MessageId = "Member")]
        protected const byte WILL = 251;
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased", MessageId = "Member")]
        protected const byte WONT = 252;
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased", MessageId = "Member")]
        protected const byte DO = 253;
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased", MessageId = "Member")]
        protected const byte DONT = 254;
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1705:LongAcronymsShouldBePascalCased", MessageId = "Member")]
        protected const byte IAC = 255;

        readonly Stream m_peer;
        bool m_ownsStream = true;
        State m_state;
        byte m_verb;
        List<byte> m_subNeg;
        bool m_readBroken;

        /// <summary>
        /// Initialize an instance.
        /// </summary>
        /// <param name="peer">A <see cref="T:System.IO.Stream"/> connected to the
        /// Telnet peer/server.
        /// </param>
        public TelnetBaseStream(Stream peer)
        {
            if (peer == null)
                throw new ArgumentNullException("peer");
            if (!peer.CanRead || !peer.CanWrite)
                throw new ArgumentException("Stream must be connected.");
            m_peer = peer;
        }

        //--------
        protected override void Dispose(bool disposing)
        {
            try {
                if (m_ownsStream) {
                    m_peer.Close();
                }
            } finally {
                base.Dispose(disposing);
            }
        }

        //--------
        /// <summary>
        /// Gets whether the incoming data is currently normal data. and we're
        /// not in the middle of a telnet command or negotiation.
        /// </summary>
        /// -
        /// <remarks>This is of most use internally.
        /// </remarks>
        public bool IsAtNormalState { get { return m_state == State.Normal; } }

        //--------
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (-1 == Array.IndexOf<byte>(buffer, 255, offset, count)) {
                m_peer.Write(buffer, offset, count);
            } else {
                // Need to escape the '255' bytes.
                byte[] tmp = Escape255s(buffer, offset, count);
                m_peer.Write(tmp, 0, tmp.Length);
            }
        }

        private static byte[] Escape255s(byte[] buffer, int offset, int count)
        {
            if (-1 == Array.IndexOf<byte>(buffer, 255, offset, count)) {
                // Not 255s needing encoding.  But at wrong offset etc?
                if (offset != 0 || count != buffer.Length) {
                    Debug.Fail("This state not tested.");
                    byte[] abs = new byte[count];
                    Array.Copy(buffer, offset, abs, 0, count);
                    return abs;
                } else {
                    return buffer;
                }
            }
            //else
            List<byte> list = new List<byte>(count + 100);
            int newCount = count;
            for (int i = 0; i < count; ++i) {
                byte cur = buffer[offset + i];
                if (cur == 255) {
                    list.Add(255);
                    ++newCount;
                }
                list.Add(cur);
            }
            byte[] tmp = list.ToArray();
            Debug.Assert(tmp.Length == newCount, "lengths!");
            return tmp;
        }

        protected void WriteTelnetCommand(byte[] buffer, int offset, int count)
        {
            TraceWriteLine("Sending answers: ", buffer, offset, count);
            m_peer.Write(buffer, offset, count);
        }

        public void SendTelnetCommand(Command command)
        {
            if (command == Command.Do || command == Command.Dont
                   || command == Command.Will || command == Command.Wont
                   || command == (Command)IAC
                   || command == Command.SB || command == Command.SE) {
                throw new ArgumentException("Sending a negotiation command value is not allowed.", "command");
            }
            TraceWriteLine("Caller sending Telnet command: {0}", command);
            byte[] buf = { IAC, checked((byte)command) };
            m_peer.Write(buf, 0, buf.Length);
        }

        public void SendTelnetSubNegotiation(byte[] buffer)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (buffer.Length == 0)
                throw new ArgumentException("buffer should not be zero length.", "buffer");
            byte[] tmpE = Escape255s(buffer, 0, buffer.Length);
            byte[] tmp2 = new byte[tmpE.Length + 4];
            tmp2[0] = IAC;
            tmp2[1] = SB;
            tmp2[(tmp2.Length - 1) - 1] = IAC;
            tmp2[(tmp2.Length - 1) - 0] = SE;
            tmpE.CopyTo(tmp2, 2);
            WriteTelnetCommand(tmp2, 0, tmp2.Length);
        }

#if SendTelnetDoDangerousDangerous
        public void SendTelnetDoDangerousDangerous(TelnetOption option)
        {
            byte[] buf;
            if (option != (TelnetOption)255) {
                buf = new byte[] { IAC, DO, checked((byte)option) };
            } else {
                buf = new byte[] { IAC, DO, 255, 255 };
            }
            TraceWriteLine("Caller sending Telnet DO command for option: {0}", option);
            m_peer.Write(buf, 0, buf.Length);
        }
#endif

        //--------
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (m_readBroken)
                throw new NotImplementedException("A previous read failed due to NotImplementedException.");
            m_answersLen = 0;
            //
            int readLen = m_peer.Read(buffer, offset, count);
            if (readLen != 0) {
                //TraceWriteLine("Received " + readLen + " bytes: [" + BitConverter.ToString(buffer, offset, readLen) + "]");
            }
            //
            int copyDiff = 0;
            for (int i = 0; i < readLen; ++i) {
                byte first = buffer[offset + i];
                bool include;
                if (m_state == State.Normal) {
                    if (first == IAC) {
                        m_state = State.SeenIac;
                        include = false;
                    } else {
                        include = true;
                    }
                } else if (m_state == State.SeenIac) {
                    if (first == DO || first == DONT || first == WILL || first == WONT) {
                        m_state = State.SeenIacVerb;
                        m_verb = first;
                        include = false;
                    } else if (first == 255) { // IAC-IAC (255-255) -> 255
                        m_state = State.Normal;
                        include = true;
                    } else if (first == SB) {
                        m_state = State.InSubNegotiation;
                        m_subNeg = new List<byte>();
                        include = false;
                    } else {
                        m_state = State.Normal;
                        include = false;
                        DoHandleCommand(first);
                    }
                } else if (m_state == State.SeenIacVerb) {
                    m_state = State.Normal;
                    include = false;
                    DoHandleNegotiation(m_verb, first);
                } else if (m_state == State.InSubNegotiation) {
                    if (first == IAC) {
                        m_state = State.InSubNegotiation_SeenIac;
                        include = false;
                    } else {
                        m_subNeg.Add(first);
                        include = false;
                    }
                } else {
                    Debug.Assert(m_state == State.InSubNegotiation_SeenIac, "Illegal state! (" + m_state + ")");
                    if (first == SE) {
                        m_state = State.Normal;
                        include = false;
                        DoHandleSubNegotiation(m_subNeg.ToArray());
                        m_subNeg = null;
                    } else if (first == 255) {
                        m_state = State.InSubNegotiation;
                        include = false;
                        m_subNeg.Add(first);
                    } else {
                        // If we get a 255 in Subneg data, but its not followed by 
                        // 255 (nor SE) then what should we assume the sender meant?
                        // That they were sending a 255 and forgot to escape it, or
                        // that they are mistakenly escaping the next byte?
                        throw NotImplementedException("IAC in SubNegotiation content but not for SE nor 255 (is 0x"
                            + first.ToString("X2", CultureInfo.InvariantCulture) + ".");
                    }
                }
                //
                if (!include) {
                    ++copyDiff;
                } else {
                    // Only need to copy if have removed some bytes previously.
                    if (copyDiff != 0) {
                        byte tmp = buffer[offset + i];
                        buffer[offset + i - copyDiff] = tmp;
                    }
                }
            }//for
            //
            // Write Negotiation responses
            if (m_answersLen != 0) {
                WriteTelnetCommand(m_answers, 0, m_answersLen);
            }
            //
            int cleanLen = readLen - copyDiff;
            if (cleanLen == 0 && readLen != 0) {
                // Must NOT return zero except at EoF.
                return this.Read(buffer, offset, count);
            } else {
                return cleanLen;
            }
        }

        protected Exception NotImplementedException(string msg)
        {
            m_readBroken = true;
            return new NotImplementedException(msg);
        }

        //--------

        public override bool CanRead
        {
            get { return m_peer.CanRead; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return m_peer.CanWrite; }
        }

        /// <summary>
        /// Clears all buffers for this stream and the child stream and
        /// causes any buffered data to be written to the underlying device.
        /// </summary>
        public override void Flush()
        {
            m_peer.Flush();
        }

        /// <summary>
        /// Not supported.
        /// </summary>
        public override long Length
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Not supported.
        /// </summary>
        public override long Position
        {
            get { throw new NotSupportedException(); }
            set { throw new NotSupportedException(); }
        }

        //--------
        enum State
        {
            Normal,
            SeenIac,
            SeenIacVerb,
            InSubNegotiation,
            InSubNegotiation_SeenIac,
        }

        protected virtual void DoHandleNegotiation(byte commandNumber, byte optionNumber)
        {
            Command command = (Command)commandNumber;
            TelnetOption option = (TelnetOption)optionNumber;
            TraceWriteLine("Received command: IAC '{0}' '{2}' (FF-{1:X2}-{3:X2})",
                command, commandNumber, option, optionNumber);
            HandleNegotiation(command, option);
        }

        protected virtual void HandleNegotiation(Command command, TelnetOption option)
        {
            switch (command) {
                case Command.Do:
                    AddResponseCommand(Command.Wont, option);
                    break;
                case Command.Dont:
                    // Always agree, but we're in the OFF state so send nothing.
                    break;
                case Command.Will:
                    AddResponseCommand(Command.Dont, option);
                    break;
                default: // WONT
                    Debug.Assert(command == Command.Wont, "More than four command types?!?");
                    // Always agree, but we're in the OFF state so send nothing.
                    break;
            }
        }

        byte[] m_answers;
        int m_answersLen;

        protected internal void AddResponseCommand(Command command, TelnetOption option)
        {
            CheckAnswersSpace(3);
            m_answers[m_answersLen + 0] = IAC;
            m_answers[m_answersLen + 1] = (byte)command;
            m_answers[m_answersLen + 2] = (byte)option;
            TraceWriteLine("Responding '{0}' '{1}' (0x{2:X2})",
                command, option, (byte)option);
            m_answersLen += 3;
        }

        protected internal void AddResponseSubNegotiation(byte[] response)
        {
            if (response == null) {
                throw new ArgumentNullException("response");
            }
            string idText = "<unknown>";
            if (response != null && response.Length != 0) { // DEBUG
                byte optionNumber = response[0];
                TelnetOption option = (TelnetOption)optionNumber;
                idText = "'" + option.ToString() + "'";
            }
            Debug.Assert(response.Length < 2 || !(response[0] == IAC && response[1] == SB),
                "We add the SB headers!");
            Debug.Assert(response.Length < 2 || !(response[response.Length - 2] == IAC && response[response.Length - 1] == SE),
                "We add the SB+SE headers!");
            TraceWriteLine("Responding with SubNeg for option " + idText + " : ", response, 0, response.Length);
            //
            byte[] rspE = Escape255s(response, 0, response.Length);
            byte[] rspC = new byte[rspE.Length + 4];
            int idx = 0;
            rspC[idx++] = IAC;
            rspC[idx++] = SB;
            rspE.CopyTo(rspC, idx);
            idx += rspE.Length;
            rspC[idx++] = IAC;
            rspC[idx++] = SE;
            Debug.Assert(idx == rspC.Length, "full asrray");
            //
            CheckAnswersSpace(rspC.Length);
            rspC.CopyTo(m_answers, m_answersLen);
            m_answersLen += rspC.Length;
        }

        private void CheckAnswersSpace(int additionalLength)
        {
            if (m_answers == null) {
                m_answers = new byte[1024];
            }
            //
            if (additionalLength + m_answersLen >= m_answers.Length) {
                throw new NotSupportedException("Too Big Options send.");
            }
        }

        //----
        private void DoHandleCommand(byte data)
        {
            TraceWriteLine("Received Command: 0x{0:X2} = {0}", data);
            HandleCommand(data);
        }

        protected virtual void HandleCommand(byte data)
        {
            TraceWriteLine("Discarding Command: 0x{0:X2} = {0}", data);
        }

        //----
        private void DoHandleSubNegotiation(byte[] data)
        {
            TraceWriteLine("Received SubNegotiation: ", data, 0, data.Length);
            HandleSubNegotiation(data);
        }

        protected virtual void HandleSubNegotiation(byte[] data)
        {
            string code = null;
            if (data.Length >= 1) {
                TelnetOption optionCode = (TelnetOption)data[0];
                code = " (" + optionCode.ToString() + ")";
            }
            TraceWriteLine("Discarding SubNegotiation" + code + ": ", data, 0, data.Length);
        }

        //--------

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32", Justification = "As in protocol")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue", Justification = "As in protocol")]
        public /*TODO move Command??*//*protected*/ enum Command : byte
        {
            SE = 240,
            //
            NOP = 241,

            DM = 242,

            /// <summary>
            /// Break
            /// </summary>
            BRK = 243,

            /// <summary>
            /// Interrupt Process
            /// </summary>
            IP = 244,

            /// <summary>
            /// Abort Output
            /// </summary>
            AO = 245,

            /// <summary>
            /// Are You There
            /// </summary>
            AYT = 246,

            /// <summary>
            /// Erase Character
            /// </summary>
            EC = 247,

            /// <summary>
            /// Erase Line
            /// </summary>
            EL = 248,

            //
            GA = 249,
            //
            SB = 250,
            //
            Do = DO,
            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dont", Justification = "As in protocol")]
            Dont = DONT,
            Will = WILL,
            Wont = WONT
        }

        protected static bool IsNegotiationCommand(Command command)
        {
            return command == Command.Do || command == Command.Dont
                || command == Command.Will || command == Command.Wont;
        }

        //--------
        /// <summary>
        /// Writes diagnostic information.
        /// </summary>
        /// -
        /// <param name="message">The descriptive message.
        /// </param>
        protected internal void TraceWriteLine(string message)
        {
            //Console.Error.WriteLine(message);
            Trace.WriteLine(message);
        }

        /// <summary>
        /// Writes diagnostic information.
        /// </summary>
        /// -
        /// <param name="format">The format string.
        /// </param>
        /// <param name="args">The arguments used by the format string.
        /// </param>
        protected internal void TraceWriteLine(string format, params object[] args)
        {
            TraceWriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
                format, args));
        }

        /// <summary>
        /// Writes diagnostic information about the content of a buffer.
        /// </summary>
        /// -
        /// <param name="prefixMessage">The descriptive message to prefix the buffer information.
        /// </param>
        /// <param name="buffer">An array of bytes containing the buffer content.
        /// </param>
        /// <param name="offset">The point at which the content begins in the byte array.
        /// </param>
        /// <param name="count">The number of bytes of content in the array.
        /// </param>
        protected internal void TraceWriteLine(string prefixMessage, byte[] buffer, int offset, int count)
        {
            string bytes = BitConverter.ToString(buffer, offset, count);
            TraceWriteLine(prefixMessage + "[ " + bytes + " ]");
        }

        /// <summary>
        /// Writes diagnostic information, optionally.
        /// </summary>
        /// -
        /// <param name="condition">A <see cref="T:System.Boolean"/> indicating
        /// whether to write the diagnostic information.
        /// </param>
        /// <param name="message">The descriptive message.
        /// </param>
        protected internal void TraceWriteLineIf(bool condition, string message)
        {
            Trace.WriteLineIf(condition, message);
        }

        //--------
        /// <summary>
        /// Not supported.
        /// </summary>
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Not supported.
        /// </summary>
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }

        //--------
        public static string UtilityShowControlChars(char ch)
        {
            return UtilityShowControlChars(ch.ToString());
        }

        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "0#str")]
        public static string UtilityShowControlChars(string str)
        {
            if (str == null)
                throw new ArgumentNullException("str");
            StringBuilder bldr = new StringBuilder(str.Length);
            foreach (char cur in str) {
                int curI = (int)cur;
                if (curI < 0x20) {
                    if (cur == '\r') {
                        bldr.Append("\\r");
                    } else if (cur == '\n') {
                        bldr.Append("\\n");
                    } else if (cur == '\t') {
                        bldr.Append("\\t");
                    } else {
                        bldr.AppendFormat(System.Globalization.CultureInfo.InvariantCulture,
                            "\\x{0:x4}", curI);
                    }
                } else {
                    bldr.Append(cur);
                }
            }
            return bldr.ToString();
        }

        internal static string Join<T>(IEnumerable<T> vars)
        {
            const string Sepa = ", ";
            StringBuilder bldr = new StringBuilder();
            foreach (T cur in vars) {
                bldr.Append(cur);
                bldr.Append(Sepa);
            }
            if (bldr.Length > Sepa.Length) {
                bldr.Length -= Sepa.Length;
            }
            return bldr.ToString();
        }

    }
}
